Skip to main content
IBM Quantum Platform

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 O=∑PcPPO = \sum_P c_P P gemessen werden soll, wobei PP Pauli-Operatoren und cPc_P Koeffizienten sind. Bezeichnen wir die Schaltung als eine einzige einheitliche Schaltung ( UU ), die logisch in die in der folgenden Abbildung dargestellten Schaltungen ( U=UCUQU = U_C U_Q ) unterteilt werden kann.

Schaltplan mit Uq gefolgt von Uc

Die Operator-Backpropagation integriert die Unitar- UCU_C in die Observable, indem sie diese wie folgt entwickelt: O′=UC†OUC=∑PcPUC†PUCO' = U_C^{\dagger}OU_C = \sum_P c_P U_C^{\dagger}PU_C. Mit anderen Worten: Ein Teil der Berechnung erfolgt klassisch durch die Entwicklung der Observable von OO zu O′O'. Das ursprüngliche Problem lässt sich nun so umformulieren, dass die Observable O′O' für den neuen Schaltkreis mit geringerer Tiefe gemessen wird, dessen Unitar UQU_Q lautet.

Das Einheits UCU_C wird als eine Reihe von Schnitten UC=USUS−1...U2U1U_C = U_S U_{S-1}...U_2U_1 dargestellt. Es gibt mehrere Möglichkeiten, einen Schnitt zu definieren. So können beispielsweise in der oben dargestellten Beispielschaltung jede Schicht der „ RzzR_{zz} “-Gates und jede Schicht der „ RxR_x “-Gates als eigenständiger „Slice“ betrachtet werden. Bei der Backpropagation werden klassischerweise die Gewichte ( O′=Πs=1S∑PcPUs†PUsO' = \Pi_{s=1}^S \sum_P c_P U_s^{\dagger} P U_s ) berechnet. Jede Schichtung UsU_s lässt sich als Us=exp(−iθsPs2)U_s = exp(\frac{-i\theta_s P_s}{2}) darstellen, wobei PsP_s ein nn -Qubit-Pauli-Operator und θs\theta_s ein Skalar ist. Es lässt sich leicht nachweisen, dass

Us†PUs=Pif [P,Ps]=0,U_s^{\dagger} P U_s = P \qquad \text{if} ~[P,P_s] = 0, Us†PUs=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

Wenn im obigen Beispiel {P,Ps}=0\{P,P_s\} = 0 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 „ PsPP_sP “ kürzen, vorausgesetzt, dass „ θs\theta_s “ 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, EstimatorOptions

Beispiel 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:

Output of the previous code cell

Als Nächstes generieren wir einen Pauli-Operator, der einen Heisenberg-XYZ-Hamiltonian modelliert:

H^XYZ=∑(j,k)∈E(Jxσjxσkx+Jyσjyσky+Jzσjzσkz)+∑j∈V(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) die Kurve der Kopplungsabbildung ist. Für dieses Tutorial haben wir Jx,Jy,JzJ_x, J_y, J_z bzw. π8,π4,π2\frac{\pi}{8}, \frac{\pi}{4}, \frac{\pi}{2} und hx,hy,hzh_x, h_y, h_z bzw. π3,π6,π9\frac{\pi}{3}, \frac{\pi}{6}, \frac{\pi}{9} 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:

Output of the previous code cell

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 „ 2L2^L “ an, wobei „ LL “ 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 O=0.1XX+0.3IZ−0.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 für diese beiden Terme zu berechnen. IZIZ ist jedoch mit den beiden anderen Termen antikommutativ, sodass wir eine separate Basismessung benötigen, um den Erwartungswert von IZIZ zu berechnen. Mit anderen Worten: Wir benötigen zwei Schaltkreise statt nur einem, um ⟨O⟩\langle O \rangle 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 MZ=1N∑i=1N⟨Zi⟩M_Z = \frac{1}{N} \sum_{i=1}^N \langle Z_i \rangle fest, wobei NN 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,
)
observable

Output:

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 O’O’ 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:
Output of the previous code cell

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$')
Output of the previous code cell

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 „ MZM_Z “ 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 „ JJ “ und „ hh “ sowie demselben Observablen „ MZM_Z “, 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 ≃0.89\simeq 0.89.

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$')
Output of the previous code cell

Nächste Schritte

Wenn Sie diese Arbeit interessant fanden, könnten Sie auch an folgenden Materialien interessiert sein:

Empfehlungen
War diese Seite hilfreich?
Melden Sie auf GitHub einen Fehler, einen Tippfehler oder fordern Sie Inhalte an.