Operator-Backpropagation (OBP) zur Schätzung von Erwartungswerten
Geschätzte Laufzeit: 4 Minuten auf einem Heron- r3 -Prozessor (HINWEIS: Dies ist nur eine Schätzung. (Die Laufzeit kann variieren.)
Lernziele
- Wie man nutzt
qiskit-addon-obp, um die Tiefe des Quantenschaltkreises auf Kosten einer erhöhten Anzahl von Schaltkreisausführungen zu verringern - Wie man zur Konstruktion von XYZ-Hamiltonianen und deren Zeitentwicklungsschaltungen verwendet
qiskit-addon-utils
Voraussetzungen
- Berechnung der Erwartungswerte einer beobachtbaren Größe mithilfe der Estimator -Primitive
Hintergrund
Die Operator-Backpropagation ist eine Technik, bei der Operationen vom Ende eines Quantenschaltkreises in die gemessene Observable integriert werden, wodurch die Tiefe des Schaltkreises im Allgemeinen verringert wird – allerdings auf Kosten zusätzlicher Terme in der Observable. Das Ziel besteht darin, so viel wie möglich des Netzwerks rückwärts zu propagieren, ohne dass die Observable zu groß wird. Eine auf Qiskit basierende Implementierung ist im OBP-Qiskit-Add-on verfügbar. Weitere Informationen finden Sie in der entsprechenden Dokumentation.
Betrachten wir eine Beispielschaltung, bei der ein Observabel gemessen werden soll, wobei Pauli-Operatoren und Koeffizienten sind. Bezeichnen wir die Schaltung als eine einzige einheitliche Schaltung ( ), die logisch in die in der folgenden Abbildung dargestellten Schaltungen ( ) unterteilt werden kann.
Die Operator-Backpropagation integriert die Unitar- in die Observable, indem sie diese wie folgt entwickelt: . Mit anderen Worten: Ein Teil der Berechnung erfolgt klassisch durch die Entwicklung der Observable von zu . Das ursprüngliche Problem lässt sich nun so umformulieren, dass die Observable für den neuen Schaltkreis mit geringerer Tiefe gemessen wird, dessen Unitar lautet.
Das Einheits wird als eine Reihe von Schnitten dargestellt. Es gibt mehrere Möglichkeiten, einen Schnitt zu definieren. So können beispielsweise in der oben dargestellten Beispielschaltung jede Schicht der „ “-Gates und jede Schicht der „ “-Gates als eigenständiger „Slice“ betrachtet werden. Bei der Backpropagation werden klassischerweise die Gewichte ( ) berechnet. Jede Schichtung lässt sich als darstellen, wobei ein -Qubit-Pauli-Operator und ein Skalar ist. Es lässt sich leicht nachweisen, dass
Wenn im obigen Beispiel gilt, müssen wir zur Berechnung des Erwartungswerts zwei Quantenkreise statt nur eines ausführen. Daher kann die Rückpropagation die Anzahl der Terme im Observablen erhöhen, was zu einer höheren Anzahl von Schaltungsausführungen führt. Eine Möglichkeit, eine tiefere Rückpropagierung in die Schaltung zu ermöglichen und gleichzeitig zu verhindern, dass der Operator zu groß wird, besteht darin, Terme mit kleinen Koeffizienten zu kürzen, anstatt sie zum Operator hinzuzufügen. Im obigen Beispiel könnte man beispielsweise den Term mit „ “ kürzen, vorausgesetzt, dass „ “ ausreichend klein ist. Das Kürzen von Termen kann zwar zu einer geringeren Anzahl auszuführender Quantenschaltungen führen, hat jedoch zur Folge, dass bei der Berechnung des endgültigen Erwartungswerts ein Fehler auftritt, der proportional zur Größe der Koeffizienten der gekürzten Terme ist.
Anforderungen
Bevor Sie mit diesem Tutorial beginnen, sollten Sie sicherstellen, dass Sie Folgendes installiert haben:
- Qiskit SDK v2.0 oder höher, mit Visual isierungsunterstützung
- Qiskit Runtime v0.22 oder später (
pip install qiskit-ibm-runtime) - OBP Qiskit-Add-on 0.3 oder höher (
pip install qiskit-addon-obp) - Qiskit-Add-on „utils“ 0.3 oder höher (
pip install qiskit-addon-utils)
Einrichtung
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, EstimatorOptionsBeispiel für einen Simulator im kleinen Maßstab
In diesem Tutorial wird ein Qiskit-Muster zur Simulation der Quantendynamik einer Heisenberg-Spin-Kette mithilfe des OBP-Qiskit-Add-ons implementiert. Beachten Sie, dass in einem rauschfreien Simulator der Erwartungswert, der mit und ohne Backpropagation ermittelt wird, identisch ist.
Schritt 1: Klassische Eingaben einem Quantenproblem zuordnen
Ordne die zeitliche Entwicklung eines Quanten-Heisenberg-Modells einem Quantenexperiment zu
Zunächst werden wir die generate_xyz_hamiltonian Funktion aus qiskit-addon-utils verwenden, um einen Heisenberg-ähnlichen Hamilton-Operator auf einem gegebenen Verbindungsgraphen zu generieren. Diese Grafik kann entweder ein „ rustworkx.PyGraph “ oder ein „ CouplingMap “ sein. Im Folgenden verwenden wir eine lineare Kette CouplingMap aus 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")Output:
Als Nächstes generieren wir einen Pauli-Operator, der einen Heisenberg-XYZ-Hamiltonian modelliert:
wobei die Kurve der Kopplungsabbildung ist. Für dieses Tutorial haben wir bzw. und bzw. verwendet.
# 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)Output:
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])
Ausgehend vom Qubit-Operator können wir eine Quantenschaltung erstellen, die dessen zeitliche Entwicklung abbildet. Wir haben die Lie-Trotter-Zerlegung verwendet generate_time_evolution_circuit , um die Zeitentwicklungsschaltung zu konstruieren.
circuit = generate_time_evolution_circuit(
hamiltonian,
time=0.2,
synthesis=LieTrotter(reps=2),
)
circuit.draw("mpl", style="iqp", fold=-1)Output:
Schritt 2: Optimierung des Problems für die Ausführung auf Quantenhardware
Erstellen Sie Schaltungsschnitte, um rückwärts zu propagieren
Die backpropagate Funktion führt die Rückwärtsausbreitung jeweils für ganze Schaltungsabschnitte durch. Daher kann die Wahl der Unterteilung Einfluss darauf haben, wie gut die Backpropagation bei einem bestimmten Problem funktioniert. Hier werden wir Gatter desselben Typs mithilfe der slice_by_depth Funktion zu Schnitten zusammenfassen.
Eine ausführlichere Erläuterung zum Thema „Circuit Slicing“ finden Sie in dieser Anleitung zum qiskit-addon-utils Paket.
slices = slice_by_depth(circuit, max_slice_depth=1)
print(f"Separated the circuit into {len(slices)} slices.")Output:
Separated the circuit into 18 slices.
Begrenze, wie groß der Operator während der Rückwärtsausbreitung werden darf
Während der Rückpropagation nähert sich die Anzahl der Terme im Operator in der Regel schnell dem Wert „ “ an, wobei „ “ die Anzahl der Schichten ist. Wenn zwei Terme im Operator auf Qubit-Ebene nicht kommutieren, benötigen wir separate Schaltkreise, um die ihnen entsprechenden Erwartungswerte zu erhalten. Wenn wir beispielsweise eine Observable mit zwei Qubits haben, dann reicht – da – eine Messung in einer einzigen Basis aus, um die Erwartungswerte für diese beiden Terme zu berechnen. ist jedoch mit den beiden anderen Termen antikommutativ, sodass wir eine separate Basismessung benötigen, um den Erwartungswert von zu berechnen. Mit anderen Worten: Wir benötigen zwei Schaltkreise statt nur einem, um zu berechnen. Mit zunehmender Anzahl der Terme im Operator steigt möglicherweise auch die erforderliche Anzahl der Schaltkreisausführungen.
Die Größe des Operators kann durch Angabe des operator_budget kwarg der Funktion backpropagate begrenzt werden, die eine OperatorBudget instanz akzeptiert.
Um den Umfang der zugewiesenen zusätzlichen Ressourcen (Anzahl der Schaltkreisläufe und damit die erforderliche QPU-Zeit) zu steuern, begrenzen wir die maximale Anzahl der qubitweisen kommutierenden Pauli-Gruppen, die die rückpropagierte Observable aufweisen darf. Hier legen wir fest, dass die Rückpropagation beendet werden soll, sobald die Anzahl der qubitweise kommutierenden Pauli-Gruppen im Operator acht übersteigt.
op_budget = OperatorBudget(max_qwc_groups=8)Slices aus dem Schaltkreis rückwärts propagieren
Zunächst legen wir die Beobachtungsgröße fest, wobei die Anzahl der Qubits ist. Wir werden die Scheiben des Zeitentwicklungskreises so lange zurückverfolgen, bis die Terme in der Beobachtungsgröße nicht mehr in acht oder weniger Qubit-weise 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,
)
observableOutput:
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])
Unten sehen Sie, dass wir sechs Schichten rückwärtsverbreitet haben und die Terme zu sechs statt zu acht Gruppen zusammengefasst wurden. Das bedeutet, dass bei einer weiteren Rückpropagierung die Anzahl der Pauli-Gruppen acht überschreiten würde. Wir können dies überprüfen, indem wir die zurückgegebenen Metadaten untersuchen. Beachten Sie außerdem, dass die Schaltungsumformung in diesem Abschnitt exakt ist. Das heißt, es wurden keine Terme der neuen Observablen abgeschnitten. Die Schaltung mit Rückwärtsausbreitung und der Operator mit Rückwärtsausbreitung liefern genau dasselbe Ergebnis wie die ursprüngliche Schaltung und der ursprüngliche 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)Output:
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 kleine Beispiel auf einem Simulator werden wir keine Rundung vornehmen. Das liegt daran, dass bei Abwesenheit von Rauschen die Schaltung mit und ohne Rückpropagation zum gleichen Ergebnis führt und die Trunkierung das Ergebnis aufgrund der zusätzlichen Approximation verschlechtert.
Die Schaltungen in den Basis-Gatter-Satz transpilieren
Nun transpilieren wir sowohl die ursprüngliche als auch die rückwärtspropagierte Schaltung in das Basisgatter des Backends. Wir müssen auf dem eigentlichen Backend nicht transpilieren, da wir für die kleine Instanz einen Simulator verwenden werden.
service = QiskitRuntimeService()
backend = service.least_busy(
operational=True, simulator=False, min_num_qubits=133
)
print(backend)Output:
<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 der ursprünglichen Schaltung bzw. der durch Rückpropagierung erzeugten Schaltung entsprechen. Anschließend führen wir die Pubs auf einem idealen Schätzer 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: Nachbearbeitung und Ausgabe des Ergebnisses im gewünschten klassischen Format
Nun berechnen wir die Erwartungswerte der ursprünglichen und der durch Rückpropagation optimierten Schaltungen.
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)Output:
Text(0, 0.5, '$M_Z$')
Wie erwartet stimmen die beiden Erwartungswerte überein. Da wir mit einem rauschfreien Statevector-Simulator arbeiten, stellt die Rückpropagierung eine exakte Transformation des Schaltkreis-Beobachtungs-Paares dar, sodass der ursprüngliche und der rückpropagierte Arbeitsablauf denselben Wert für „ “ ergeben müssen. Der Vorteil der Rückpropagierung zeigt sich erst auf rauschbehafteter Hardware, wo der kürzere rückpropagierte Schaltkreis weniger Fehler akkumuliert, wie das nachstehende Beispiel für groß angelegte Hardware veranschaulicht.
Beispiel für groß angelegte Hardware
Bei der Entwicklung eines Experiments ist es sinnvoll, mit einer kleinen Schaltung zu beginnen, um Visualisierungen und Simulationen zu vereinfachen. Nun betrachten wir die Operator-Backpropagation für einen 50-Qubit-Heisenberg-Hamiltonian mit denselben Werten für die Parameter „ “ und „ “ sowie demselben Observablen „ “, jedoch für vier Trotter-Schritte. Der ideale Erwartungswert lässt sich in diesem Maßstab nicht mit einer Brute-Force-Methode berechnen, daher verwenden wir ein Tensornetzwerk und erhalten als idealen Erwartungswert den Wert .
Neben der Rückwärtsausbreitung stellen wir in diesem groß angelegten Beispiel auch die Rückwärtsausbreitung mit Trunkierung vor. Idealerweise möchten wir die Rückpropagierung so weit wie möglich vornehmen, um die Tiefe des effektiven Schaltkreises zu verringern. Dies führt jedoch häufig zu einer großen Anzahl von nicht-kommutierenden Termen in der aktualisierten Observable, was den Quanten-Overhead erhöht. Daher können wir beobachtbare Terme mit kleinen Koeffizienten mithilfe einer Methode namens Trunkierung eliminieren. Zwar ermöglicht die Trunkierung eine stärkere Ausbreitung, indem sie die Anzahl der Terme in der aktualisierten Observablen verringert, doch führt sie auch zu einer gewissen Annäherung. Daher ist es notwendig, die Kürzung auf ein gewisses Maß zu beschränken, damit der Approximationsfehler die durch eine tiefere Backpropagation erzielte Rauschreduzierung nicht zunichte macht.
Um den Umfang der Trunkierung zu begrenzen, weisen wir jedem Segment ein Fehlerbudget zu und legen mithilfe der setup_budget Funktion ein Gesamtfehlerbudget für die gesamte Schaltung fest, die der Rückwärtsausbreitung unterliegt. Dadurch wird sichergestellt, dass die Abschaltung sowohl für jeden einzelnen Abschnitt als auch für die gesamte Schaltung gesteuert wird. Weitere Möglichkeiten zur Aufteilung des Budgets finden Sie 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()Output:
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}")Output:
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)Output:
Text(0, 0.5, '$M_Z$')
Nächste Schritte
Wenn Sie diese Arbeit interessant fanden, könnten Sie auch an folgenden Materialien interessiert sein:
- Ungefähre Quantenkompilierung für Zeitentwicklungsschaltungen
- Mehrproduktformeln zur Verringerung des Trotter-Fehlers
pauli-prop, ein mit Rust beschleunigtes Paket für die Pauli-Propagation, mit Tutorials zu OBP, klassischer Erwartungswertschätzung und Simulation unter Rauschen