Operator-Rückwärtspropagation (OBP) zur Schätzung von Erwartungswerten
Nutzungsschätzung: 4 Minuten auf einem Heron r3-Prozessor (HINWEIS: Dies ist nur eine Schätzung. Deine Laufzeit kann abweichen.)
Lernziele
Nach diesem Tutorial sollten Nutzer verstehen:
- Wie man
qiskit-addon-obpeinsetzt, um die Tiefe des Quantenschaltkreises auf Kosten einer erhöhten Anzahl von Schaltkreisausführungen zu reduzieren - Wie man
qiskit-addon-utilsverwendet, um XYZ-Hamiltonians und ihre Zeitentwicklungsschaltkreise zu konstruieren
Voraussetzungen
Wir empfehlen, dass Nutzer mit folgenden Themen vertraut sind, bevor sie dieses Tutorial durcharbeiten:
- Verwendung des Estimator-Primitives zur Berechnung von Erwartungswerten eines Observablen
Hintergrund
Operator-Rückwärtspropagation ist eine Technik, bei der Operationen vom Ende eines Quantenschaltkreises in das gemessene Observable absorbiert werden, wodurch die Tiefe des Schaltkreises in der Regel auf Kosten zusätzlicher Terme im Observable reduziert wird. Das Ziel ist es, so viel des Schaltkreises wie möglich rückwärts zu propagieren, ohne zu erlauben, dass das Observable zu groß wird. Eine Qiskit-basierte Implementierung ist im OBP-Qiskit-Addon verfügbar. Lies die entsprechende Dokumentation für weitere Informationen.
Betrachte einen Beispielschaltkreis, für den ein Observable gemessen werden soll, wobei Paulis und Koeffizienten sind. Wir bezeichnen den Schaltkreis als einen einzelnen unitären Operator , der logisch in unterteilt werden kann, wie in der folgenden Abbildung gezeigt.

Die Operator-Rückwärtspropagation absorbiert den unitären Operator in das Observable, indem er als entwickelt wird. Mit anderen Worten: Ein Teil der Berechnung wird klassisch durch die Entwicklung des Observablen von zu durchgeführt. Das ursprüngliche Problem kann nun neu formuliert werden als Messung des Observablen für den neuen Schaltkreis mit geringerer Tiefe, dessen unitärer Operator ist.
Der unitäre Operator wird als eine Anzahl von Schichten dargestellt. Es gibt mehrere Möglichkeiten, eine Schicht zu definieren. Im obigen Beispielschaltkreis kann beispielsweise jede Schicht von - und jede Schicht von -Gates als eine einzelne Schicht betrachtet werden. Die Rückwärtspropagation beinhaltet die klassische Berechnung von . Jede Schicht kann als dargestellt werden, wobei ein -Qubit-Pauli und ein Skalar ist. Es ist leicht zu überprüfen, dass
Im obigen Beispiel, wenn , müssen wir zwei Quantenschaltkreise anstelle von einem ausführen, um den Erwartungswert zu berechnen. Daher kann die Rückwärtspropagation die Anzahl der Terme im Observable erhöhen, was zu einer höheren Anzahl von Schaltkreisausführungen führt. Eine Möglichkeit, eine tiefere Rückwärtspropagation in den Schaltkreis zu ermöglichen und gleichzeitig zu verhindern, dass der Operator zu groß wird, besteht darin, Terme mit kleinen Koeffizienten abzuschneiden, anstatt sie zum Operator hinzuzufügen. Im obigen Beispiel könnte man beispielsweise den Term mit abschneiden, sofern hinreichend klein ist. Das Abschneiden von Termen kann zu weniger auszuführenden Quantenschaltkreisen führen, führt aber zu einem gewissen Fehler bei der endgültigen Erwartungswertberechnung, der proportional zur Größe der Koeffizienten der abgeschnittenen Terme ist.
Anforderungen
Stelle vor Beginn dieses Tutorials sicher, dass du folgendes installiert hast:
- Qiskit SDK v2.0 oder später, mit Visualisierungs-Unterstützung
- Qiskit Runtime v0.22 oder später (
pip install qiskit-ibm-runtime) - OBP-Qiskit-Addon 0.3 oder später (
pip install qiskit-addon-obp) - Qiskit-Addon-Utils 0.3 oder später (
pip install qiskit-addon-utils)
Setup
# 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
Kleinskaliges Simulator-Beispiel
Dieses Tutorial implementiert ein Qiskit-Muster zur Simulation der Quantendynamik einer Heisenberg-Spinkette mit dem OBP-Qiskit-Addon. Beachte, dass in einem rauschfreien Simulator der mit und ohne Rückwärtspropagation erhaltene Erwartungswert gleich ist.
Schritt 1: Klassische Eingaben auf ein Quantenproblem abbilden
Die Zeitentwicklung eines quantenmechanischen Heisenberg-Modells auf ein Quantenexperiment abbilden
Zunächst verwenden wir die Funktion generate_xyz_hamiltonian aus qiskit-addon-utils, um einen heisenbergartigen Hamiltonian auf einem gegebenen Konnektivitätsgraphen zu erzeugen. Dieser Graph kann entweder ein rustworkx.PyGraph oder eine CouplingMap sein. Im Folgenden verwenden wir eine lineare Ketten-CouplingMap mit 10 Qubits.
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")
Als nächstes erzeugen wir einen Pauli-Operator, der einen Heisenberg-XYZ-Hamiltonian modelliert:
wobei der Graph der Kopplungskarte ist. Für dieses Tutorial haben wir gleich und gleich gewählt.
# 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])
Aus dem Qubit-Operator können wir einen Quantenschaltkreis erzeugen, der seine Zeitentwicklung modelliert. Wir haben generate_time_evolution_circuit mit Lie-Trotter-Zerlegung verwendet, um den Zeitentwicklungsschaltkreis zu konstruieren.
circuit = generate_time_evolution_circuit(
hamiltonian,
time=0.2,
synthesis=LieTrotter(reps=2),
)
circuit.draw("mpl", style="iqp", fold=-1)

Schritt 2: Problem für die Ausführung auf Quantenhardware optimieren
Schaltkreisschichten für die Rückwärtspropagation erstellen
Die Funktion backpropagate propagiert jeweils ganze Schaltkreisschichten rückwärts. Daher kann die Wahl der Schichtaufteilung einen Einfluss darauf haben, wie gut die Rückwärtspropagation für ein gegebenes Problem funktioniert. Hier gruppieren wir Gates desselben Typs mit der Funktion slice_by_depth in Schichten.
Für eine ausführlichere Diskussion zur Schaltkreis-Schichtaufteilung, schaue dir diesen How-to-Leitfaden des Pakets qiskit-addon-utils an.
slices = slice_by_depth(circuit, max_slice_depth=1)
print(f"Separated the circuit into {len(slices)} slices.")
Separated the circuit into 18 slices.
Begrenzen, wie groß der Operator während der Rückwärtspropagation wachsen kann
Während der Rückwärtspropagation nähert sich die Anzahl der Terme im Operator im Allgemeinen schnell an, wobei die Anzahl der Schichten ist. Wenn zwei Terme im Operator qubitweise nicht kommutieren, benötigen wir separate Schaltkreise, um die entsprechenden Erwartungswerte zu erhalten. Wenn wir beispielsweise ein zweiQubit-Observable haben, dann reicht, da , eine Messung in einer einzigen Basis aus, um die Erwartungswerte dieser beiden Terme zu berechnen. Jedoch anti-kommutiert mit den anderen beiden Termen, weshalb wir eine separate Basismessung benötigen, um den Erwartungswert von zu berechnen. Mit anderen Worten: Wir benötigen zwei Schaltkreise anstelle von einem, um zu berechnen. Wenn die Anzahl der Terme im Operator zunimmt, besteht die Möglichkeit, dass auch die erforderliche Anzahl von Schaltkreisausführungen zunimmt.
Die Größe des Operators kann durch Angabe des operator_budget-Parameters der Funktion backpropagate begrenzt werden, der eine OperatorBudget-Instanz akzeptiert.
Um die Menge der zusätzlichen Ressourcen (Anzahl der Schaltkreisausführungen und damit die benötigte QPU-Zeit) zu steuern, begrenzen wir die maximale Anzahl qubitweise kommutierender Pauli-Gruppen, die das rückwärtspropagierte Observable haben darf. Hier legen wir fest, dass die Rückwärtspropagation stoppen soll, wenn die Anzahl der qubitweise kommutierenden Pauli-Gruppen im Operator über acht hinausgeht.
op_budget = OperatorBudget(max_qwc_groups=8)
Schichten aus dem Schaltkreis rückwärtspropagieren
Zunächst legen wir das Observable als fest, wobei die Anzahl der Qubits ist. Wir werden Schichten aus dem Zeitentwicklungsschaltkreis rückwärtspropagieren, bis die Terme im Observable nicht mehr in acht oder weniger qubitweise kommutierende Pauli-Gruppen zusammengefasst werden können.
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])
Im Folgenden wirst du sehen, dass wir sechs Schichten rückwärtspropagiert haben und die Terme in sechs und nicht in acht Gruppen zusammengefasst wurden. Dies bedeutet, dass das Rückwärtspropagieren einer weiteren Schicht dazu führen würde, dass die Anzahl der Pauli-Gruppen acht übersteigt. Wir können überprüfen, ob dies der Fall ist, indem wir die zurückgegebenen Metadaten inspizieren. Beachte auch, dass in diesem Abschnitt die Schaltkreistransformation exakt ist. Das heißt, es wurden keine Terme des neuen Observablen abgeschnitten. Der rückwärtspropagierte Schaltkreis und der rückwärtspropagierte Operator liefern das exakt gleiche Ergebnis wie der ursprüngliche Schaltkreis und Operator.
# 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:
Für das kleinskalige Beispiel auf einem Simulator werden wir keine Abschneidung verwenden. Das liegt daran, dass in Abwesenheit von Rauschen der Schaltkreis mit und ohne Rückwärtspropagation zum gleichen Ergebnis führt und die Abschneidung das Ergebnis aufgrund der hinzukommenden Approximation verschlechtert.
Die Schaltkreise in den Basis-Gate-Satz transpilieren
Jetzt transpilieren wir sowohl den ursprünglichen als auch den rückwärtspropagieren Schaltkreis in das Basis-Gate des Backends. Wir müssen nicht auf dem eigentlichen Backend transpilieren, da wir für die kleine Instanz auf einem Simulator ausführen werden.
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)
Schritt 3: Mit Qiskit-Primitives ausführen
Zunächst erstellen wir zwei Primitive Unified Blocs (PUBs), die dem ursprünglichen Schaltkreis und dem rückwärtspropagieren Schaltkreis entsprechen. Dann führen wir die PUBs auf einem idealen Estimator aus, um die Erwartungswerte zu erhalten.
pubs = [(isa_circuit, observable), (isa_bp_circuit, bp_obs)]
rng = np.random.default_rng()
estimator = StatevectorEstimator(seed=rng)
job = estimator.run(pubs)
Schritt 4: Nachverarbeitung und Rückgabe des Ergebnisses im gewünschten klassischen Format
Jetzt erhalten wir die Erwartungswerte des ursprünglichen und des rückwärtspropagieren Schaltkreises.
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$')
Wie erwartet stimmen die beiden Erwartungswerte überein. Da wir auf einem rauschfreien Zustandsvektor-Simulator laufen, ist die Rückwärtspropagation eine exakte Transformation des Schaltkreis-Observable-Paares, sodass die ursprünglichen und rückwärtspropagieren Arbeitsabläufe denselben Wert von liefern müssen. Der Vorteil der Rückwärtspropagation wird erst auf verrauschter Hardware deutlich, wo der kürzere rückwärtspropagiere Schaltkreis weniger Fehler ansammelt, wie im großskaligen Hardware-Beispiel unten veranschaulicht.
Großskaliges Hardware-Beispiel
Bei der Entwicklung eines Experiments ist es nützlich, mit einem kleinen Schaltkreis zu beginnen, um Visualisierungen und Simulationen zu erleichtern. Jetzt betrachten wir die Operator-Rückwärtspropagation für einen 50-Qubit-Heisenberg-Hamiltonian mit denselben Werten für die - und -Parameter und dasselbe Observable , aber für vier Trotter-Schritte. Der ideale Erwartungswert bei dieser Größenordnung kann nicht mit einer Brute-Force-Methode berechnet werden, also verwenden wir ein Tensornetz und erhalten den idealen Erwartungswert von .
Neben der Rückwärtspropagation führen wir in diesem großskaligen Beispiel auch die Rückwärtspropagation mit Abschneidung ein. Im Idealfall möchten wir so viel wie möglich rückwärtspropagieren, um die Tiefe des effektiven Schaltkreises zu reduzieren. Dies führt jedoch oft zu einer großen Anzahl nicht-kommutierender Terme im aktualisierten Observable, was den Quantenaufwand erhöht. Daher können wir Observable-Terme mit kleinen Koeffizienten durch eine Methode namens Abschneidung eliminieren. Während die Abschneidung eine tiefere Propagation ermöglicht, indem sie die Anzahl der Terme im aktualisierten Observable reduziert, führt sie auch zu einer gewissen Approximation. Daher ist es notwendig, die Abschneidung innerhalb bestimmter Grenzen zu beschränken, damit der Approximationsfehler nicht die Reduzierung des Rauschens durch tiefere Rückwärtspropagation überwiegt.
Um die Menge der Abschneidung zu begrenzen, weisen wir mit der Funktion setup_budget ein Fehlerbudget für jede Schicht sowie das Gesamtfehlerbudget über den gesamten rückwärtspropagieren Schaltkreis zu. Dies stellt sicher, dass die Abschneidung für jede Schicht sowie für den gesamten Schaltkreis kontrolliert wird. Weitere Möglichkeiten zur Budgetverteilung findest du auch in diesem Leitfaden.
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$')
Nächste Schritte
Falls du diese Arbeit interessant findest, könntest du auch an folgendem Material interessiert sein:
- Approximate quantum compilation for time evolution circuits
- Multi-product formulas to reduce Trotter error
pauli-prop, ein Rust-beschleunigtes Paket für Pauli-Propagation, mit Tutorials, die OBP, klassische Erwartungswert-Schätzung und verrauschte Simulation abdecken