Zum Hauptinhalt springen

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-obp einsetzt, um die Tiefe des Quantenschaltkreises auf Kosten einer erhöhten Anzahl von Schaltkreisausführungen zu reduzieren
  • Wie man qiskit-addon-utils verwendet, 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 O=PcPPO = \sum_P c_P P gemessen werden soll, wobei PP Paulis und cPc_P Koeffizienten sind. Wir bezeichnen den Schaltkreis als einen einzelnen unitären Operator UU, der logisch in U=UCUQU = U_C U_Q unterteilt werden kann, wie in der folgenden Abbildung gezeigt.

Schaltkreisdiagramm, das Uq gefolgt von Uc zeigt

Die Operator-Rückwärtspropagation absorbiert den unitären Operator UCU_C in das Observable, indem er als O=UCOUC=PcPUCPUCO' = U_C^{\dagger}OU_C = \sum_P c_P U_C^{\dagger}PU_C entwickelt wird. Mit anderen Worten: Ein Teil der Berechnung wird klassisch durch die Entwicklung des Observablen von OO zu OO' durchgeführt. Das ursprüngliche Problem kann nun neu formuliert werden als Messung des Observablen OO' für den neuen Schaltkreis mit geringerer Tiefe, dessen unitärer Operator UQU_Q ist.

Der unitäre Operator UCU_C wird als eine Anzahl von Schichten UC=USUS1...U2U1U_C = U_S U_{S-1}...U_2U_1 dargestellt. Es gibt mehrere Möglichkeiten, eine Schicht zu definieren. Im obigen Beispielschaltkreis kann beispielsweise jede Schicht von RzzR_{zz}- und jede Schicht von RxR_x-Gates als eine einzelne Schicht betrachtet werden. Die Rückwärtspropagation beinhaltet die klassische Berechnung von O=Πs=1SPcPUsPUsO' = \Pi_{s=1}^S \sum_P c_P U_s^{\dagger} P U_s. Jede Schicht UsU_s kann als Us=exp(iθsPs2)U_s = exp(\frac{-i\theta_s P_s}{2}) dargestellt werden, wobei PsP_s ein nn-Qubit-Pauli und θs\theta_s ein Skalar ist. Es ist leicht zu überprüfen, dass

UsPUs=Pif [P,Ps]=0,U_s^{\dagger} P U_s = P \qquad \text{if} ~[P,P_s] = 0, UsPUs=cos(θs)P+isin(θs)PsPif {P,Ps}=0U_s^{\dagger} P U_s = \qquad cos(\theta_s)P + i sin(\theta_s)P_sP \qquad \text{if} ~\{P,P_s\} = 0

Im obigen Beispiel, wenn {P,Ps}=0\{P,P_s\} = 0, 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 PsPP_sP abschneiden, sofern θs\theta_s 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")

Ausgabe der vorherigen Code-Zelle

Als nächstes erzeugen wir einen Pauli-Operator, der einen Heisenberg-XYZ-Hamiltonian modelliert:

H^XYZ=(j,k)E(Jxσjxσkx+Jyσjyσky+Jzσjzσkz)+jV(hxσjx+hyσjy+hzσjz),{\hat{\mathcal{H}}_{XYZ} = \sum_{(j,k)\in E} (J_{x} \sigma_j^{x} \sigma_{k}^{x} + J_{y} \sigma_j^{y} \sigma_{k}^{y} + J_{z} \sigma_j^{z} \sigma_{k}^{z}) + \sum_{j\in V} (h_{x} \sigma_j^{x} + h_{y} \sigma_j^{y} + h_{z} \sigma_j^{z}),}

wobei G(V,E)G(V,E) der Graph der Kopplungskarte ist. Für dieses Tutorial haben wir Jx,Jy,JzJ_x, J_y, J_z gleich π8,π4,π2\frac{\pi}{8}, \frac{\pi}{4}, \frac{\pi}{2} und hx,hy,hzh_x, h_y, h_z gleich π3,π6,π9\frac{\pi}{3}, \frac{\pi}{6}, \frac{\pi}{9} 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)

Ausgabe der vorherigen Code-Zelle

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 2L2^L an, wobei LL 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 O=0.1XX+0.3IZ0.5IXO = 0.1 XX + 0.3 IZ - 0.5 IX haben, dann reicht, da [XX,IX]=0[XX,IX] = 0, eine Messung in einer einzigen Basis aus, um die Erwartungswerte dieser beiden Terme zu berechnen. Jedoch anti-kommutiert IZIZ mit den anderen beiden Termen, weshalb wir eine separate Basismessung benötigen, um den Erwartungswert von IZIZ zu berechnen. Mit anderen Worten: Wir benötigen zwei Schaltkreise anstelle von einem, um O\langle O \rangle 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 MZ=1Ni=1NZiM_Z = \frac{1}{N} \sum_{i=1}^N \langle Z_i \rangle fest, wobei NN 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 OO' 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 {len(bp_obs.group_commuting(qubit_wise=True))} groups."
)
print(
f"Note that backpropagating one more slice would result in {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:

Ausgabe der vorherigen Code-Zelle

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$')

Ausgabe der vorherigen Code-Zelle

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 MZM_Z 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 JJ- und hh-Parameter und dasselbe Observable MZM_Z, 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 0.89\simeq 0.89.

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: {isa_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
f"2-qubit depth with backpropagation: {isa_bp_circuit.depth(lambda x: x.operation.num_qubits == 2)}"
)
print(
f"2-qubit depth with backpropagation and truncation: {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$')

Ausgabe der vorherigen Code-Zelle

Nächste Schritte

Falls du diese Arbeit interessant findest, könntest du auch an folgendem Material interessiert sein:

Empfehlungen